#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Simple script for mass-fetching lyrics for lyricwiki and 
# tagging mp3 with id3v2.[34] tags.
#
# Copyright (c) 2008, umat
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without 
# modification, are permitted provided that the following conditions are met:
#
# - Redistributions of source code must retain the above copyright notice, this 
# list of conditions and the following disclaimer.
# - Redistributions in binary form must reproduce the above copyright notice, 
# this list of conditions and the following disclaimer in the documentation 
# and/or other materials provided with the distribution.
# - The names of the contributors may not be used to endorse or promote products 
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


#### REQUIREMENTS: #####

# - ZSI Soap library (tested with version 2.0)
# - eyeD3 Tag library (tested with version 0.6.16)

import os, sys, getopt, re, difflib, codecs
from stat import ST_MODE, S_ISDIR, S_ISREG
import LyricWiki_services
import eyeD3
from eyeD3.frames import *
import urllib


from LyUtil import ttywidth, getch, black, red, green, yellow, blue, mangenta, cyan, white, bold

stats = {}


def decode_ascii(text):
	text = urllib.unquote(text)
	text = unicode(text, "latin-1").encode("latin-1")
	text = unicode(text, "utf-8")
	text = unicode(text.encode("latin-1"), "utf-8")
	return text


def get_lyrics(artist, title, remove_parentheses=False, substitute_symbols=True):
    try:
	soap = LyricWiki_services.LyricWikiBindingSOAP("http://lyricwiki.org/server.php")
        song = LyricWiki_services.getSongRequest()

        artist = re.compile('[\[\]]').sub('', artist) # Remove parentheses
        artist = re.compile('^:').sub('', artist) # Remove colon at end and beginning
        artist = re.compile(':$').sub('', artist) 
        song.Artist = artist.encode("utf-8")
        song.Artist = urllib.quote(song.Artist)

        if substitute_symbols:
            title = title.replace("+","(Plus)")
            title = title.replace("=","(Equals)")

        title = title.replace("[","(")
        title = title.replace("]",")")
        if remove_parentheses:
            title = re.compile('\(.*?\)').sub('', title) # Remove parentheses
        #title = re.compile('\[.*?\]').sub('', title) # emove box brackets
        song.Song = title.encode("utf-8") # Convert to plain string
        song.Song = urllib.quote(song.Song) # Escape Songname
	result = soap.getSong(song)

	# Encoding is wrong. Lyrics are actually ecoded UTF-8 twice
	# So reinterpret them as Latin-1 and finally convert them to proper UTF-8.
	#lyrics = result.Return.Lyrics
	#print len(lyrics)
	#for i in lyrics:
	#    print "%c(%X) " % (i, ord(i)),
	#print
	lyrics = decode_ascii(result.Return.Lyrics)
	try:
	    url = decode_ascii(result.Return.Url)
	except:
	    url = result.Return.Url
        #print bold(black("\nDEBUG: Request: %s by %s => %s" % (song.Song, song.Artist, url)))
    except Exception, ex:
	lyrics = "Not found"
	url = "Failed!"
	cla, exc, trbk = sys.exc_info()
	print red("(%s: %s)" % (cla.__name__, str(exc))),

        import traceback
        print >>sys.stderr, "Uncaught exception:", str(ex)
        print >>sys.stderr, traceback.format_exc()


    return lyrics, url
    

def draw_line():
    r = ''
    for i in range(0, ttywidth-1): r = r + '-'
    print blue(r)


def format_compare_line(line, width):
    add_color = chr(0x1b)+"[32m"
    remove_color = chr(0x1b)+"[31m"
    change_color = chr(0x1b)+"[33m"
    reset_color = chr(0x1b)+"[36m"

    line_w = line.count("\0") * 2 + line.count("\1")
    line = line.replace('\t','    ').replace('\n','')[:width+line_w-1]
    line = line.replace('\0+',add_color). \
                replace('\0-',remove_color). \
                replace('\0^',change_color). \
                replace('\1',reset_color)
    line = line + bold(black(u'¶'))
    return line

def compare_lyrics(fromlines, tolines):
    global assume_yes
    width = ttywidth / 2 - 2

    fromlines = fromlines.split('\n')
    tolines = tolines.split('\n')

    context_lines = max(len(fromlines),len(tolines))
    diff = difflib._mdiff(fromlines,tolines,context_lines)
    draw_line()
    set_pos = chr(0x1b)+"["+str(width)+"G"
    for left, right, changes in diff:
        left = format_compare_line(left[1], width)
        right = format_compare_line(right[1], width)
        if changes:
            marker = red("!")
        else:
            marker = " "
        result = cyan(left)+set_pos+blue(" |")+marker+cyan(right)
        print result
    draw_line()
    if assume_yes:
        return True
    print "\nUse "+cyan("right (new)")+" lyrics?",
    sys.stdout.flush()
    result = (getch() == "y")
    if result:
        print green("Yes.")
    else:
        print red("No.")
    return result


def compare_lyrics_old(lyricsA, lyricsB):
    width = ttywidth / 2 - 2
    sideA = lyricsA.split("\n")
    sideB = lyricsB.split("\n")

    draw_line()
    for i in range(0,max(len(sideA), len(sideB))):
	if len(sideA) > i: 
	    rowA = sideA[i]
	    if len(rowA) > width: rowA = rowA[:width]
	else:
	    rowA = ""
	if len(sideB) > i: 
	    rowB = sideB[i]
	    if len(rowB) > width: rowB = rowB[:width]
	else:
	    rowB = ""

	format = cyan("%%-%ds" % width)+blue(" | ")+cyan("%%-%ds" % width)
	print format % (rowA, rowB)
    draw_line()
    print "\nUse "+cyan("right (new)")+" lyrics?",
    sys.stdout.flush()
    result = (getch() == "y")
    if result:
	print green("Yes.")
    else:
	print red("No.")
    return result


def clean_text(s):
    # replace some common chars with a sufficient latin-1 equivalent
    s = s.replace(u'‘',u"'")
    s = s.replace(u'’',u"'")
    s = s.replace(u'–',u'-')
    s = s.replace(u'…',u'...')
    s = s.replace(u'”',u'"')
    return s


def update_lyrics(filename, filecount, totalfilecount):
    global verbose, only_empty, quiet, stats, update, force_utf_16
    tag = eyeD3.Tag()
    tag.link(filename)

    cnt = "[%d/%d]" % (filecount, totalfilecount)
    if only_empty and len(tag.getLyrics()) > 0:
	stats["exist"] = stats["exist"] + 1
	if not quiet:
	    print "%s Skipping %s by %s ..." % (bold(black(cnt)), bold(tag.getTitle()), bold(tag.getArtist())),
	    print green("lyrics exist")
	return

    print "%s Fetching lyrics for %s by %s ..." % (bold(black(cnt)), bold(tag.getTitle()), bold(tag.getArtist())),
    sys.stdout.flush()

    newlyrics, url = get_lyrics(tag.getArtist(), tag.getTitle())
    if newlyrics == "Not found": # Give it another try without parantheses. Removes things like (feat. ...) etc.
        newlyrics, url = get_lyrics(tag.getArtist(), tag.getTitle(), remove_parentheses=True)


    if newlyrics == "Not found":
        if len(tag.getLyrics()) > 0:
	    stats["present"] = stats["present"] + 1
	    print red("not found")+green(" but we have the lyrics already.")
	else:
	    stats["failed"] = stats["failed"] + 1
	    print "%s, see %s" % (red("not found"), bold(blue(url)))
    else:
	# check if latin-1 is sufficient (with some basic clean-ups)
	# this way we stay id3v2.3 compatible as it's missing utf-8 support
        newlyrics = newlyrics.strip( unicode( codecs.BOM_UTF8, "latin-1" ) )
	newlyrics_latin1 = clean_text(newlyrics).encode("latin-1", "replace").decode("latin-1")
	if clean_text(newlyrics) == newlyrics_latin1 and not force_utf_16 and not tag.encoding == UTF_16_ENCODING:
	    newlyrics = newlyrics_latin1
	    encoding = LATIN1_ENCODING
	else:
	    print bold(black("(using UTF-16)")),
	    encoding = UTF_16_ENCODING

        useupdate = True
        if len(tag.getLyrics()) > 0:
	    # Just use first lyrics field, and ensure unix line breaks
	    lyrics = tag.getLyrics()[0].lyrics.replace('\r\n', '\n')
	    if (lyrics == newlyrics):
		stats["same"] = stats["same"] + 1
		print green("found and current lyrics are the same.")
		useupdate = False
	    else:
		stats["differ"] = stats["differ"] + 1
		print yellow("found, but current lyrics differ.")
	        useupdate = update or compare_lyrics(lyrics, newlyrics)
        else:
	    print green("found.")

	if useupdate:
	    print "Writing tag for %s ..." % (bold(filename)),
	    try:
		tag.removeLyrics() # prevent duplicate lyrics
		newlyrics = tag.strToUnicode(newlyrics.replace('\n','\r\n'))
		tag.frames.setLyricsFrame(newlyrics, u"", "eng", encoding)
		tag.update()
		print green("done.")
		stats["ok"] = stats["ok"] + 1
	    except Exception, ex:
		stats["writefailed"] = stats["writefailed"] + 1
		cla, exc, trbk = sys.exc_info()
		print red("(%s: %s)" % (cla.__name__, str(exc))),
		print red("failed.")
                import traceback
                print >>sys.stderr, "Uncaught exception:", str(ex)
                print >>sys.stderr, traceback.format_exc()

	    
def process_list(filenames):
    filecount = 0
    for file in filenames:
	filecount = filecount + 1
	update_lyrics(file, filecount, len(filenames))

def info():
    progname = os.path.basename(sys.argv[0])
    print "%s - simple interactive console based lyrics fetcher" % bold(progname)
    print


def usage():
    progname = os.path.basename(sys.argv[0])
    print "Usage: %s [OPTION] [DIRECTORY or FILE]..." % bold(progname)
    print "\nFetches lyrics for songs from %s." % bold(blue("http://lyricswiki.org"))
    print "Processes all given mp3 files and directories with all it's subdirectories."
    print "\n  %s, %-20s  only fetch lyrics for files that have no lyrics in the tags" % ( bold("-e"), bold("--only-empty") )
    print "\n  %s, %-20s  decrease status output" % ( bold("-q"), bold("--quiet") )
    print "\n  %s, %-20s  always update tags from lyricwiki if available" % ( bold("-u"), bold("--update") )
    print "\n  %s, %-20s  force UTF-16 encoding for tags" % ( bold("-U"), bold("--utf-16") )
    print "\n  %s, %-20s  assume yes on every prompt" % ( bold("-y"), bold("--yes") )
    print "\nBy default, lyrics for all files are fetched and you will be prompted if the"
    print "current lyrics differ from the ones from %s." % bold(blue("http://lyricswiki.org"))
    print "\n"+bold(red("Use at own risk! Don't blame me for destroyed files or tags!"))
    print 

def print_stats():
    global stats

    help = { "present" : yellow("Lyrics not found on lyricwiki but already present in tags"),
	     "exist" : green("Lyrics already in tags"),
	     "failed" : red("Lyrics not found and not available in tags"),
	     "same" : green("Lyrics match the ones from lyricwiki"),
	     "differ" : yellow("Lyrics differ from the fone from lyricswiki"),
	     "ok" : green("Lyrics successfully updated"),
	     "writefailed" : red("Failed write operations")
	   }
    for k in stats:
	if stats[k] > 0:
	    print help[k] + bold(black(": ")) + bold(str(stats[k]))
    
def main():
    global verbose, only_empty, stats, quiet, update, force_utf_16, assume_yes
    # initalize stats
    info()
    stats["present"] = stats["exist"] = stats["failed"] = stats["same"] = stats["differ"] = stats["ok"] = stats["writefailed"] = 0
    try:
        opts, args = getopt.getopt(sys.argv[1:], "hveyquU", ["help", "only-empty", "quiet", "update", "utf-16", "yes"])
    except getopt.GetoptError, err:
        print "Error: %s" % red(str(err))
        usage()
        return 2
    verbose = False
    only_empty = False
    quiet = False
    update = False
    force_utf_16 = False
    assume_yes = False
    for o, a in opts:
        if o == "-v":
            verbose = True # not used yet...
        elif o in ("-h", "--help"):
            usage()
            return 0
        elif o in ("-e", "--only-empty"):
            only_empty = True
        elif o in ("-q", "--quiet"):
            quiet = True
        elif o in ("-u", "--update"):
            update = True
        elif o in ("-U", "--utf-16"):
            force_utf_16 = True
        elif o in ("-y", "--yes"):
            assume_yes = True
        else:
            assert False, "unhandled option"

    if len(args) == 0: 
	usage()
	return 2

    filelist = []
    for path in args:
	try:
	    mode = os.stat(path)[ST_MODE]
	    isdir = S_ISDIR(mode)
	    isfile = S_ISREG(mode) and path.endswith(".mp3")
	except Exception:
    	    #cla, exc, trbk = sys.exc_info()
	    #print red("(%s: %s)" % (cla.__name__, str(exc))),
	    isdir = False
	    isfile = False

	if isdir:
	    for root, dirs, files in os.walk(path):
		for name in files:
		    if name.endswith(".mp3"):
			filelist.append(os.path.join(root, name))
	elif isfile:
	    filelist.append(path)
	else:
	    print "%s is not a file or directory" % bold(path)
	    print

    process_list(filelist)
    print_stats()

if __name__ == "__main__":
    retval = 0

    try:
	retval = main();
    except KeyboardInterrupt:
        retval = 0
    except Exception, ex:
        import traceback
        print >>sys.stderr, "Uncaught exception:", str(ex)
        print >>sys.stderr, traceback.format_exc()

    sys.exit(retval)
